using System;
using System.Runtime.InteropServices;
using System.Threading;
using Microsoft.Win32.SafeHandles;

namespace IppServer.Host.Printing
{
    internal static class WindowsPrinterDispatcher
    {
        public static void SendToPrinter(string printerName, byte[] documentData, string documentName, CancellationToken cancellationToken)
        {
            if (documentData == null || documentData.Length == 0)
            {
                throw new ArgumentException("Document data is empty.", nameof(documentData));
            }

            using (var printerHandle = OpenPrinterHandle(printerName))
            {
                var docInfo = new DOC_INFO_1
                {
                    pDocName = documentName,
                    pDatatype = "RAW",
                    pOutputFile = null
                };

                if (!NativeMethods.StartDocPrinter(printerHandle, 1, ref docInfo))
                {
                    throw new InvalidOperationException($"StartDocPrinter failed: {Marshal.GetLastWin32Error()}");
                }

                if (!NativeMethods.StartPagePrinter(printerHandle))
                {
                    NativeMethods.EndDocPrinter(printerHandle);
                    throw new InvalidOperationException($"StartPagePrinter failed: {Marshal.GetLastWin32Error()}");
                }

                const int chunkSize = 32 * 1024;
                var buffer = new byte[Math.Min(chunkSize, documentData.Length)];
                try
                {
                    var offset = 0;
                    while (offset < documentData.Length)
                    {
                        cancellationToken.ThrowIfCancellationRequested();

                        var toWrite = Math.Min(chunkSize, documentData.Length - offset);
                        Buffer.BlockCopy(documentData, offset, buffer, 0, toWrite);

                        if (!NativeMethods.WritePrinter(printerHandle, buffer, toWrite, out var written) || written != toWrite)
                        {
                            throw new InvalidOperationException($"WritePrinter failed: {Marshal.GetLastWin32Error()}");
                        }

                        offset += toWrite;
                    }
                }
                finally
                {
                    NativeMethods.EndPagePrinter(printerHandle);
                    NativeMethods.EndDocPrinter(printerHandle);
                }
            }
        }

        private static SafePrinterHandle OpenPrinterHandle(string printerName)
        {
            if (!NativeMethods.OpenPrinter(printerName, out var handle, IntPtr.Zero))
            {
                throw new InvalidOperationException($"无法打开打印机 \"{printerName}\": {Marshal.GetLastWin32Error()}");
            }

            return handle;
        }

        private static class NativeMethods
        {
            [DllImport("winspool.drv", SetLastError = true, CharSet = CharSet.Unicode)]
            internal static extern bool OpenPrinter(string pPrinterName, out SafePrinterHandle phPrinter, IntPtr pDefault);

            [DllImport("winspool.drv", SetLastError = true)]
            internal static extern bool ClosePrinter(IntPtr hPrinter);

            [DllImport("winspool.drv", SetLastError = true, CharSet = CharSet.Unicode)]
            internal static extern bool StartDocPrinter(SafePrinterHandle hPrinter, int level, [In] ref DOC_INFO_1 pDocInfo);

            [DllImport("winspool.drv", SetLastError = true)]
            internal static extern bool EndDocPrinter(SafePrinterHandle hPrinter);

            [DllImport("winspool.drv", SetLastError = true)]
            internal static extern bool StartPagePrinter(SafePrinterHandle hPrinter);

            [DllImport("winspool.drv", SetLastError = true)]
            internal static extern bool EndPagePrinter(SafePrinterHandle hPrinter);

            [DllImport("winspool.drv", SetLastError = true)]
            internal static extern bool WritePrinter(SafePrinterHandle hPrinter, byte[] pBytes, int dwCount, out int dwWritten);
        }

        [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
        private struct DOC_INFO_1
        {
            [MarshalAs(UnmanagedType.LPWStr)]
            public string pDocName;

            [MarshalAs(UnmanagedType.LPWStr)]
            public string pOutputFile;

            [MarshalAs(UnmanagedType.LPWStr)]
            public string pDatatype;
        }

        private sealed class SafePrinterHandle : SafeHandleZeroOrMinusOneIsInvalid
        {
            public SafePrinterHandle() : base(true)
            {
            }

            protected override bool ReleaseHandle()
            {
                return NativeMethods.ClosePrinter(handle);
            }
        }
    }
}

